#include "main.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <libgen.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "log.h"
#include "filelock.h"
#include "io.h"
#include "task.h"
#include "addr_ntop.h"
#include "socks/tcp.h"

struct G G = {
        .listenPort = "1080",
};

static void usage(const char *prog) {
    fprintf(stdout, "Usage: %s [-4[ipv4 listen addr]] [-6[ipv6 listen addr]]"
            " [-p<listen port>]\n"
            "\t-4 --ipv4\n"
            "\t-6 --ipv6\n"
            "\t-p --port\n",
            prog);
    exit(EXIT_FAILURE);
}

static void igetopt(int argc, char *argv[]) {
    int c;
    char listenon = 0;

#define IPV4 0x01
#define IPV6 0x02

    while (1) {
        int optIdx = 0;
        static struct option options[] = {
                { "help", no_argument, 0, 'h' },
                { "ipv4", optional_argument, 0, '4'},
                { "ipv6", optional_argument, 0, '6'},
                { "port", required_argument, 0, 'p'},
                { NULL, 0, NULL, 0 }
        };

        c = getopt_long(argc, argv, "4::6::hp:", options, &optIdx);
        if (c == -1)
            break;

        switch (c) {
        case 0: case 'h': case '?':
            usage(argv[0]);
            break;
        case '4':
            G.listenIpv4 = optarg;
            listenon |= IPV4;
            break;
        case '6':
            G.listenIpv6 = optarg;
            listenon |= IPV6;
            break;
        case 'p':
            G.listenPort = optarg;
            break;
        default:
            printf("?? getopt returned character code 0%o ??", c);
            break;
        }
    }

    if (optind < argc) {
        printf("non-option ARGV-elements: ");
        while (optind < argc)
            printf("%s ", argv[optind++]);
        printf("\n\n");
        usage(argv[0]);
    }

    if (listenon) {
        if ((listenon & IPV4) && G.listenIpv4 == NULL)
            G.listenIpv4 = "0.0.0.0";
        if ((listenon & IPV6) && G.listenIpv6 == NULL)
            G.listenIpv6 = "::";
    } else {
        G.listenIpv4 = "0.0.0.0";
        G.listenIpv6 = "::";
    }

#undef IPV4
#undef IPV6
}

static void daemonize(const char *cmd) {
#if !DBG
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    /* Clear file creation mask. */
    umask(0);

    /* Get maximum number of file descriptors. */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
        fprintf(stderr, "%s: can't get file limit: getrlimit error: %s\n",
                cmd, strerror(errno));
        exit(EXIT_FAILURE);
    }

    /* Become a session leader to lose controlling TTY. */
    if ((pid = fork()) < 0) {
        fprintf(stderr, "%s: can't fork: %s\n", cmd, strerror(errno));
        exit(EXIT_FAILURE);
    } else if (pid != 0) /* parent */
        exit(EXIT_SUCCESS);
    setsid();

    /* Ensure future opens won't allocate controlling TTYs. */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        fprintf(stderr, "%s: can't ignore SIGHUP\n", cmd);
        exit(EXIT_FAILURE);
    }
    if ((pid = fork()) < 0) {
        fprintf(stderr, "%s: can't fork: %s\n", cmd, strerror(errno));
        exit(EXIT_FAILURE);
    } else if (pid != 0) /* parent */
        exit(EXIT_SUCCESS);

    /* Change the current working directory to the root so
     * we won't prevent file systems from being unmounted. */
    if (chdir("/") < 0) {
        fprintf(stderr, "%s: can't change directory to /\n", cmd);
        exit(EXIT_FAILURE);
    }

    /* Close all open file descriptors. */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; (unsigned int)i < rl.rlim_max; i++)
        close(i);

    /* Attach file descriptors 0, 1, and 2 to /dev/null. */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    /* Initialize the log file. */
    OPENLOG(cmd);
    LOG(LOG_INFO, "is startup");
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        LOG(LOG_ALERT, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
        exit(EXIT_FAILURE);
    }
#endif
}

static int alreadyRunning(const char *ident, int *lckfd) {
    int fd;
    char buf[16];
    char lockfilename[256];

#define LOCKFILE_BASE "/var/run/%s.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)

    snprintf(lockfilename, sizeof(lockfilename), LOCKFILE_BASE, ident);

    if ((fd = open(lockfilename, O_RDWR|O_CREAT, LOCKMODE)) < 0) {
        LOG(LOG_ALERT, "can't open lock file %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (write_lock(fd, 0, SEEK_SET, 0) < 0) {
        if (errno == EACCES || errno == EAGAIN) {
            close(fd);
            return 1;
        }
        LOG(LOG_ALERT, "can't lock %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (ftruncate(fd, 0) < 0) {
        LOG(LOG_ALERT, "can't truncate lock file %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }
    snprintf(buf, sizeof(buf), "%ld", (long)getpid());
    if (writeLowLevel(fd, buf, strlen(buf) + 1) < 0) {
        LOG(LOG_ALERT, "can't write pid to %s: %s", lockfilename, strerror(errno));
        exit(EXIT_FAILURE);
    }

    if (lckfd)
        *lckfd = fd;

#undef LOCKFILE_BASE
#undef LOCKMODE

    return 0;
}

static void sigCleanupHandler(int signo) {
    switch (signo) {
    case SIGINT:
        LOG(LOG_INFO, "got SIGINT, exiting...");
        break;
    case SIGTERM:
        LOG(LOG_INFO, "got SIGTERM, exiting...");
        break;
    case SIGQUIT:
        LOG(LOG_INFO, "got SIGQUIT, exiting...");
        break;
    default:
        return;
    }

    exit(EXIT_SUCCESS);
}

static void sigChldHandler(int signo __attribute((unused))) {
    pid_t pid;
    int stat;

    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
//        LOG(LOG_INFO, "child %d terminated", pid);
    }
}

static int sigactions(void) {
    struct sigaction sa;

    sa.sa_handler = sigCleanupHandler;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sa.sa_flags = 0;
    if (sigaction(SIGINT, &sa, NULL) < 0) {
        LOG(LOG_CRIT, "Can't catch SIGINT: %s", strerror(errno));
        return -1;
    }
    sa.sa_handler = sigCleanupHandler;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGQUIT);
    sa.sa_flags = 0;
    if (sigaction(SIGTERM, &sa, NULL) < 0) {
        LOG(LOG_CRIT, "Can't catch SIGTERM: %s", strerror(errno));
        return -1;
    }
    sa.sa_handler = sigCleanupHandler;
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sa.sa_flags = 0;
    if (sigaction(SIGQUIT, &sa, NULL) < 0) {
        LOG(LOG_CRIT, "Can't catch SIGQUIT: %s", strerror(errno));
        return -1;
    }

    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGPIPE, &sa, NULL) < 0) {
        LOG(LOG_CRIT, "Can't ignore SIGPIPE: %s", strerror(errno));
        return -1;
    }

    sa.sa_handler = sigChldHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGCHLD, &sa, NULL) < 0) {
        LOG(LOG_CRIT, "Can't catch SIGCHLD: %s", strerror(errno));
        return -1;
    }

    return 0;
}

int main(int argc, char *argv[]) {
    G.progName = basename(argv[0]);
	igetopt(argc, argv);
    daemonize(basename(argv[0]));
    if (alreadyRunning(G.progName, NULL)) {
        LOG(LOG_CRIT, "already running");
        exit(EXIT_FAILURE);
    }
    sigactions();

    doTcpSocks(&G);

	return EXIT_SUCCESS;
}
